package jmine.tec.test.utils.spring;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;

import jmine.tec.test.junit3.EnhancedTestCase;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.beans.factory.access.SingletonBeanFactoryLocator;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author takeshi
 */
public abstract class BeanFactoryAwareTestCase extends EnhancedTestCase {
    /**
     * 
     */
    public static final int NUM_10 = 10;

    public static final Object BEAN_FACTORY_REFERENCE = new Object();

    public static final Object SPRING_BEANS_KEY = new Object();

    public static final String SPRING_BEANS_XML = "jmine-tec-integration-test.xml";

    public static final String SPRING_REF_FACTORY = "classpath:beanRefFactory.xml";

    private boolean autoWire;

    private final boolean preferSingleton;

    private final String resourcePath;

    private long startTime;

    private boolean managed = false;

    private String beanRefFactory;

    private BeanFactoryLocator factoryLocator;

    private BeanFactoryReference factoryReference; // TODO fix this

    /**
     * C'tor. Specifies a beanRefFactory file of choice instead of the default which is "classpath:beanRefFactory.xml". You may want to use
     * this if you happen to have more than one factory xml in your classpath and don't want them to be mixed. i.e. name them differently
     * and specify here which one to use.
     * 
     * @param beanRefFactory String
     * @param resourcePath String
     * @param autoWire boolean
     * @param preferSingleton boolean
     */
    public BeanFactoryAwareTestCase(final String beanRefFactory, final String resourcePath, final boolean autoWire,
            final boolean preferSingleton) {
        this(resourcePath, autoWire, preferSingleton);
        this.beanRefFactory = beanRefFactory;
    }

    /**
     * C'tor
     * 
     * @param resourcePath {@link String}
     * @param autoWire boolean
     * @param preferSingleton boolean
     */
    public BeanFactoryAwareTestCase(final String resourcePath, final boolean autoWire, final boolean preferSingleton) {
        super();
        this.autoWire = autoWire;
        this.preferSingleton = preferSingleton;
        this.resourcePath = resourcePath;
        this.managed = !preferSingleton;
        this.beanRefFactory = SPRING_REF_FACTORY;
    }

    /**
     * C'tor
     * 
     * @param resourcePath {@link String}
     * @param autoWire boolean
     * @param factoryLocator the {@link BeanFactoryLocator}
     */
    public BeanFactoryAwareTestCase(final String resourcePath, final boolean autoWire, final BeanFactoryLocator factoryLocator) {
        super();
        this.resourcePath = resourcePath;
        this.autoWire = autoWire;
        this.factoryLocator = factoryLocator;
        this.preferSingleton = true;
    }

    /**
     * Autowires fields by name, checking type compatibility
     */
    public void autoWire() {
        for (final Field field : this.getClass().getDeclaredFields()) {
            final String name = field.getName();
            if (this.getBeanFactory().containsBean(name)) {
                Class<?> type = this.getBeanFactory().getType(name);
                if (type != null && field.getType().isAssignableFrom(type) && !Modifier.isFinal(field.getModifiers())) {
                    try {
                        AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                            public Void run() throws Exception {
                                field.setAccessible(true);
                                field.set(BeanFactoryAwareTestCase.this, BeanFactoryAwareTestCase.this.getBean(name));
                                return null;
                            }
                        });
                    } catch (Exception e) {
                        System.err.println(e.getMessage());
                    }
                }
            }
        }
    }

    /**
     * Creates the BeanFactory if necessary
     * 
     * @param resource the location of the config file
     */
    private void createBeanFactory(final String resource) {
        synchronized (EnhancedSpringTestCase.class) {
            ListableBeanFactory bf = new ClassPathXmlApplicationContext(resource);
            this.setSharedResource(SPRING_BEANS_KEY, bf);
            this.managed = true;
        }
    }

    /**
     * Forces freeing of instance fields
     */
    protected final void freeFields() {
        final Object target = this;
        Class<?> targetClass = this.getClass();
        this.freeFieldsFrom(target, targetClass);
        // while (targetClass != null && targetClass !=
        // BeanFactoryAwareTestCase.class) {
        // freeFieldsFrom(target, targetClass);
        // targetClass = targetClass.getSuperclass();
        // }
        this.freeFieldsFrom(target, BeanFactoryAwareTestCase.class);
    }

    /**
     * @param target the target object
     * @param targetClass the target class
     */
    private void freeFieldsFrom(final Object target, final Class<?> targetClass) {
        for (final Field field : targetClass.getDeclaredFields()) {
            if (field.getType().isPrimitive()) {
                continue;
            }
            if (!field.getType().getCanonicalName().startsWith("java.lang")
                    && (field.getModifiers() & (Modifier.STATIC | Modifier.FINAL)) == 0) {
                AccessController.doPrivileged(new PrivilegedAction<Object>() {
                    public Object run() {
                        field.setAccessible(true);
                        try {
                            field.set(target, null);
                        } catch (IllegalArgumentException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                        return null;
                    }
                });
            }
        }
    }

    /**
     * @param beanName String
     * @return Object
     */
    public Object getBean(final String beanName) {
        return this.getBeanFactory().getBean(beanName);
    }

    /**
     * @param <E> the type
     * @param beanName the bean name
     * @param cls the class
     * @return E
     */
    public <E> E getBean(final String beanName, final Class<? extends E> cls) {
        return cls.cast(this.getBeanFactory().getBean(beanName, cls));
    }

    /**
     * @return the beanFactory
     */
    public final ListableBeanFactory getBeanFactory() {
        ListableBeanFactory sharedResource = (ListableBeanFactory) this.getSharedResource(SPRING_BEANS_KEY);
        if (sharedResource == null) {
            this.createBeanFactory(this.resourcePath);
        }
        return (ListableBeanFactory) this.getSharedResource(SPRING_BEANS_KEY);
    }

    /**
     * @return the autoWire
     */
    public boolean isAutoWire() {
        return this.autoWire;
    }

    /**
     * @param autoWire the autoWire to set
     */
    public void setAutoWire(final boolean autoWire) {
        this.autoWire = autoWire;
    }

    /**
     * 
     * 
     */
    private void setSharedBeanFactory() {
        try {
            BeanFactoryReference reference = this.getBeanFactoryReference();
            this.setSharedResource(BEAN_FACTORY_REFERENCE, reference);
            this.setSharedResource(SPRING_BEANS_KEY, reference.getFactory());
        } catch (RuntimeException e) {
            e.printStackTrace();
            this.managed = true;
        }
    }

    /**
     * @return {@link BeanFactoryReference}
     */
    private BeanFactoryReference getBeanFactoryReference() {
        if (this.factoryReference == null) {
            Object sharedResource = this.getSharedResource(BEAN_FACTORY_REFERENCE);
            if (sharedResource != null) {
                this.factoryReference = (BeanFactoryReference) sharedResource;
            } else { // building a beanFactoryReference
                this.factoryReference = this.getBeanFactoryLocator().useBeanFactory(this.resourcePath);
                this.setSharedResource(BEAN_FACTORY_REFERENCE, this.factoryReference);
            }
        }
        return this.factoryReference;
    }

    /**
     * @return {@link BeanFactoryLocator}
     */
    private BeanFactoryLocator getBeanFactoryLocator() {
        if (this.factoryLocator == null) {
            this.factoryLocator = SingletonBeanFactoryLocator.getInstance(this.beanRefFactory);
            // this.factoryLocator = SingletonBeanFactoryLocator.getInstance();
        }
        return this.factoryLocator;
    }

    /**
     * @throws Exception e
     */
    @Override
    protected final void setUpBeforeClass() throws Exception {
        super.setUpBeforeClass();
        if (this.preferSingleton) {
            this.setSharedBeanFactory();
        }
        this.setUpBeforeClassInternal();
    }

    /**
     * @throws Exception e
     */
    @Override
    protected void setUpBeforeTest() throws Exception {
        super.setUpBeforeTest();
        this.startTime = System.currentTimeMillis();
        if (this.isAutoWire()) {
            this.autoWire();
        }
    }

    /**
     * @throws Exception e
     */
    @Override
    protected final void tearDownAfterClass() throws Exception {
        try {
            this.tearDownAfterClassInternal();
        } finally {
            if (this.managed) {
                if (this.getBeanFactory() != null && this.getBeanFactory() instanceof DisposableBean) {
                    ((DisposableBean) this.getBeanFactory()).destroy();
                }
            } else if (this.getBeanFactoryReference() != null) {
                this.getBeanFactoryReference().release();
            }
            this.freeFields();
        }
    }

    /**
     * To be overwritten in subclasses.
     * 
     * @throws Exception e
     */
    protected void tearDownAfterClassInternal() throws Exception {
        // vazio
    }

    /**
     * @throws Exception e
     */
    protected void setUpBeforeClassInternal() throws Exception {
        // vazio

    }

    /**
     * @throws Exception e
     */
    @Override
    protected void tearDownAfterTest() throws Exception {
        System.out.println("Test: " + this.getName() + " took: " + (System.currentTimeMillis() - this.startTime));
    }

}
